import traceback
from pathlib import Path
from loguru import logger
from collections import defaultdict
from xml.etree import ElementTree
from dataclasses import dataclass, field
from typing import Dict, List, Any, Optional, Union

from utils.loader import FileProcessor


class DataMapperParser:
    @classmethod
    def run_parser(
            cls,
            source_data: List[Dict],
            mapping: Dict[str, str],
            exclude_value: Union[str, list, tuple, set, None] = None,
            ext_fields: Optional[dict] = None,
            same_file: Optional[bool] = None
    ) -> List[Dict]:
        """
        按照映射关系，把原始数据映射成符合模型要求的结构

        :param source_data: 原始数据
        :param mapping: 映射关系
        :param exclude_value: 排除特殊值
        :param ext_fields: 扩展字段，会被添加到目标字典中
        :param same_file: 如果为True，则key和value都是在source_data取值作为新的key和value
        :return: 映射后的数据列表
        """
        # 1. 输入校验
        if not isinstance(source_data, list):
            raise TypeError(f'期望数据类型: list，实际传入：{type(source_data)}')

        expect_data_json = []
        error_items = []  # 记录处理失败的项目
        if exclude_value is not None:
            exclude_value = set(exclude_value) if isinstance(exclude_value, str) else set(exclude_value)
        # 2. 处理每个数据项
        for idx, item in enumerate(source_data):
            try:
                # 2.1 校验项目类型
                if not isinstance(item, dict):
                    raise TypeError(f"索引 {idx} 的项目不是字典类型，实际类型: {type(item)}")
                expect_data_dict = {}
                # 2.2 处理映射关系
                for key, source_key in mapping.items():
                    # 获取源值
                    if source_key not in item:
                        logger.warning(f"键 '{source_key}' 在索引 {idx} 的项目中不存在")
                        continue
                    value = item[source_key]
                    if exclude_value and value in exclude_value:
                        logger.warning(f"索引 {idx} 的键 '{source_key}' 值为存在特殊值列表中{exclude_value}")
                        continue
                    # 处理是否在同一个文件取值
                    key = item[key] if same_file else key
                    try:
                        # 添加到目标字典
                        expect_data_dict[key] = value
                    except TypeError:
                        raise KeyError(f"{key}添加到目标字段是异常，数据类型{type(key)}")
                # 2.3 添加扩展字段
                if ext_fields and isinstance(ext_fields, dict):
                    expect_data_dict.update(ext_fields)

                # 2.4 添加到结果列表
                expect_data_json.append(expect_data_dict)

            except Exception as e:
                # 3. 捕获异常并记录详细上下文
                error_context = cls._get_error_context(idx, item, e)
                error_items.append(error_context)
                logger.warning(f"数据处理异常: {error_context['error_message']}")

        # 4. 记录处理结果
        total_items = len(source_data)
        success_items = len(expect_data_json)
        error_count = len(error_items)

        logger.info(
            f"数据解析完成: 总数={total_items}, 成功={success_items}, 失败={error_count}"
        )

        # 5. 如果有错误，记录详细错误信息
        if error_items:
            logger.debug("处理失败的项目详情:")
            for error in error_items:
                logger.debug(
                    f"索引 {error['index']}: "
                    f"错误类型={error['error_type']}, "
                    f"错误信息={error['error_message']}, "
                    f"项目内容={error['item_preview']}"
                )

        return expect_data_json

    @staticmethod
    def _get_error_context(
            index: int,
            item: Any,
            exception: Exception
    ) -> Dict[str, Any]:
        """
        获取异常上下文信息

        :param index: 项目索引
        :param item: 原始项目数据
        :param exception: 捕获的异常
        :return: 包含详细上下文信息的字典
        """
        # 截取项目预览（避免过大）
        item_preview = str(item)[:200] + ('...' if len(str(item)) > 200 else '')

        # 获取堆栈跟踪的最后几行
        stack_trace = traceback.format_exc()
        stack_lines = stack_trace.strip().split('\n')
        relevant_stack = '\n'.join(stack_lines[-4:]) if len(stack_lines) > 4 else stack_trace

        return {
            "index": index,
            "error_type": type(exception).__name__,
            "error_message": str(exception),
            "item_type": type(item).__name__,
            "item_preview": item_preview,
            "stack_trace": relevant_stack,
            "full_exception": repr(exception)
        }


class MediaOfnParser(DataMapperParser):
    @dataclass
    class MovieNfo:
        """电影信息对象容器"""
        # 剧情描述相关字段
        plot: Optional[str] = None  # 剧情简介（可能包含CDATA格式）
        outline: Optional[str] = None  # 剧情概要（通常比plot更简短）
        originalplot: Optional[str] = None  # 原始剧情描述（未翻译版本）
        tagline: Optional[str] = None  # 宣传标语（如"发行日期 2023-10-31"）

        # 发行日期相关字段
        premiered: Optional[str] = None  # 首映日期
        releasedate: Optional[str] = None  # 发布日期
        release: Optional[str] = None  # 发行日期（通常与releasedate相同）

        # 影片标识信息
        num: Optional[str] = None  # 影片编号（如"ADN-506"）
        title: Optional[str] = None  # 影片标题
        originaltitle: Optional[str] = None  # 原始标题（未翻译版本）
        sorttitle: Optional[str] = None  # 用于排序的标题（可能包含编号前缀）

        # 分级与地区信息
        mpaa: Optional[str] = None  # MPAA分级（如"JP-18+"）
        customrating: Optional[str] = None  # 自定义分级（通常与mpaa相同）
        countrycode: Optional[str] = None  # 国家/地区代码（如"JP"）

        # 演职人员信息
        actors: List[str] = field(default_factory=list)  # 演员列表（每个演员是字典，包含name和type）
        director: Optional[str] = None  # 导演姓名

        # 评分与年份信息
        rating: Optional[float] = None  # 用户评分（如4.0）
        criticrating: Optional[int] = None  # 专业评分（如40，可能表示4.0 * 10）
        votes: Optional[int] = None  # 打分人数
        year: Optional[int] = None  # 发行年份

        # 时长和制作信息
        runtime: Optional[int] = None  # 影片时长（分钟）
        studio: Optional[str] = None  # 制作公司/工作室
        maker: Optional[str] = None  # 制作方（通常与studio相同）
        publisher: Optional[str] = None  # 发行商
        label: Optional[str] = None  # 品牌/标签（通常与publisher相同）

        # 分类标签
        tags: List[str] = field(default_factory=list)  # 标签列表（如"AVC1", "1080P", "不倫"等）
        genres: List[str] = field(default_factory=list)  # 类型/流派列表（如"人妻·主婦", "寢取られ"等）
        series: str = None

        # 媒体资源链接
        poster: Optional[str] = None  # 海报图片URL
        cover: Optional[str] = None  # 封面图片URL
        trailer: Optional[str] = None  # 预告片视频URL
        website: Optional[str] = None  # 官方网站URL

        # 特定平台标识
        other: Optional[dict] = field(default_factory=dict)  # JAVDB平台搜索ID（通常与num相同）

        def __repr__(self) -> str:
            """自定义对象表示形式"""
            return f"<MovieInfo {self.num}: {self.title}>"

        def to_dict(self) -> Dict[str, Any]:
            """转换为字典格式"""
            return {
                k: v for k, v in self.__dict__.items()
                if not k.startswith('__') and not callable(v)
            }

    def parse_xml_to_object(self, path: Path | str) -> MovieNfo:
        """
        将XML内容解析为MovieInfo对象

        :return: MovieInfo对象
        """
        # 读取nfo文件内容
        nfo = FileProcessor(path)
        nfo.load()

        movie_info = self.MovieNfo()
        # 提取重复元素处理器
        multi_value_tags = defaultdict(list)

        # 遍历所有XML元素
        for element in nfo.root:
            tag = element.tag
            text = element.text.strip() if element.text else None

            # 处理CDATA内容
            if element.text and "<![CDATA[" in ElementTree.tostring(element, encoding='unicode'):
                text = element.text.strip() if element.text else None

            # 处理特殊类型转换
            if tag == 'rating' and text:
                movie_info.rating = float(text)
            elif tag == 'criticrating' and text:
                movie_info.criticrating = int(text)
            elif tag == 'year' and text:
                movie_info.year = int(text)
            elif tag == 'runtime' and text:
                movie_info.runtime = int(text)
            elif tag in {'tag', 'genre'} and text:
                multi_value_tags[tag].append(text)
            elif tag == 'actor':
                # 处理演员嵌套结构
                for sub_element in element:
                    if sub_element.tag == 'name':
                        if sub_element.text:
                            movie_info.actors.append(sub_element.text)
            elif tag == 'series':
                movie_info.series = text
            else:
                # 简单元素直接赋值
                if hasattr(movie_info, tag):
                    setattr(movie_info, tag, text)
                else:
                    movie_info.other[tag] = text
        # 添加多值标签
        movie_info.tags = multi_value_tags.get('tag', [])
        movie_info.genres = multi_value_tags.get('genre', [])
        return movie_info

    def export_nfo_obj_json(self, path_list: [Path, str], export_path: Optional[str] = None):
        """解析nfo文件，导出模型关系需要的关系json文件
        :param path_list 记录媒体文件路径的数据
        :param export_path 数据导出路径
        """
        tags, company, mpaa, director, series = (set() for _ in range(5))
        media_parse_obj = {}
        for nfo_item in path_list:
            # 解析数据
            nfo_item = Path(nfo_item)
            media_obj = self.parse_xml_to_object(nfo_item)
            media_obj.dir_name = nfo_item.stem
            media_parse_obj[nfo_item.stem] = media_obj.to_dict()

            # 收集数据
            self._add_values_to_set(media_obj, tags, 'tags', 'genres')
            self._add_values_to_set(media_obj, company, 'studio', 'maker', 'publisher', 'label')
            self._add_values_to_set(media_obj, mpaa, 'mpaa')
            self._add_values_to_set(media_obj, director, 'director')
            self._add_values_to_set(media_obj, director, 'director')
            logger.info('已解析文件-{}', nfo_item)
        logger.info('所有文件解析完成，总数:{}-成功:{}', len(path_list), len(media_parse_obj))

        json_obj = {
            'total': len(media_parse_obj),
            'items': media_parse_obj,
            'tags': list(tags),
            'company': list(company),
            'mpaa': list(mpaa),
            'director': list(director)

        }
        if export_path:
            FileProcessor(export_path, model='w').save(data=json_obj)

        return export_path or json_obj

    @classmethod
    def _add_values_to_set(cls, obj, set_obj, *attrs):
        """将对象的多个属性值添加到集合"""

        for attr in attrs:
            if value := getattr(obj, attr, None):
                if isinstance(value, str):
                    set_obj.add(value)
                if isinstance(value, list):
                    set_obj.update(value)


if __name__ == '__main__':
    import json

    test_path = r'E:\PythonProject\yun-cool-cinema\cache\MIDE-163-C.nfo'
    media = MediaOfnParser()
    result = media.export_nfo_obj_json([test_path])
    print(json.dumps(result, indent=3, ensure_ascii=False))
